################################################################################
# Sheep Dreams
# IGDA Global Game Jam 2009
# Team Awesome
################################################################################

##### IMPORTS #####
from pandac.PandaModules import *
import direct.directbase.DirectStart
from direct.gui.DirectGui import *
from direct.showbase.DirectObject import DirectObject
from direct.interval.IntervalGlobal import *
from direct.fsm import FSM
from direct.fsm import State
from direct.task import Task
from direct.actor.Actor import Actor
from direct.filter.CommonFilters import CommonFilters
import random
import math
import sys

##### MAGIC NUMBERS DEFINITION #####
RUNNINGSPEED = 10
TURNINGSPEED = 3

FRICTION_COEFF = 0.03125

GRAVITY = -10 * 20

SHOWCOLL = False

MAX_SHEEP = 30

MVX = 1
MVY = 2
MVCAM = 3

# Animation States
ANIM_WALK_START = 0
ANIM_WALK = 1
ANIM_WALK_STOP = 2
ANIM_JUMP_UP = 3
ANIM_JUMP_DOWN = 4
ANIM_FLOAT_UP = 5
ANIM_FLOAT_DOWN = 6
ANIM_FLAIL = 7
ANIM_WAKE_UP = 8

# Pose States
POSE_STAND = 9
POSE_JUMP_UP = 10
POSE_FLOAT = 11
POSE_ASLEEP = 12
POSE_WALK = 13
POSE_LOOP = 14

# Animations Number of Frames
ANIM_FRAMES = [10,
               40,
               10,
               45,
               45,
               10,
               10,
               40,
               40]

# Animations End Poses
ANIM_END_POSE = [POSE_WALK,
                 POSE_LOOP,
                 POSE_STAND,
                 POSE_JUMP_UP,
                 POSE_STAND,
                 POSE_FLOAT,
                 POSE_STAND,
                 POSE_LOOP,
                 POSE_STAND]
               
# Animations Strings
ANIM_STRINGS = ["start_walk",
                "walk_loop",
                "stop_walk",
                "jump_up",
                "jump_down",
                "float_up",
                "float_down",
                "flail",
                "asleep"]

##### SHEEP BALL CLASS DEFINITION #####
class SheepBall(NodePath):
    def __init__(self, name):
        NodePath.__init__(self, name)
        
        # Initialize attributes
        
        self.accel = Vec3(0,0,0)
        self.vel = Vec3(0,0,0)
                
        
        self.groundZ = 0
        
        self.radius = 2
        
        self.camrot = 0
        
        self.startPos = self.getPos()
        
        self.camFollowNode = NodePath('camFollow')
        self.camFollowNode.setPos(0, 10, 20)
        self.camFollowNode.reparentTo(self)
        
        # Load the model
#        self.model = loader.loadModel("models/gorilla.egg")
        self.model = NodePath('model')
        self.model.setPos(0, 0, 0)
        self.model.setScale(1)
        self.model.reparentTo(self)
        
        self.dummy = Actor("models/clock_mod.egg", {'anim':'models/clock_ani.egg'})
        self.dummy.setScale(0.1)
        self.dummy.reparentTo(self.model)
        
        # Load the texture for the model
#        tex = loader.loadTexture('textures/beach_ball_t.png')
#        self.model.setTexture(tex)
        
        # Setup Collision Node for bumping into objects in the environment
        self.modelCS = CollisionSphere(0,0,0,self.radius)
        self.modelCN = CollisionNode("model")
        self.modelCN.addSolid(self.modelCS)
        self.modelCN.setFromCollideMask(BitMask32.bit(0))
        self.modelCN.setIntoCollideMask(BitMask32.allOff())
        self.modelCNP = NodePath(self.modelCN)
        self.modelCNP.reparentTo(self.model)
        if(SHOWCOLL): self.modelCNP.show()
        
        # Setup Collision Ray for ground detection
        self.modelGroundCR = CollisionRay()
        self.modelGroundCR.setOrigin(0,0,1000)
        self.modelGroundCR.setDirection(0,0,-1)
        self.modelGroundCN = CollisionNode(name + 'groundRay')
        self.modelGroundCN.addSolid(self.modelGroundCR)
        self.modelGroundCN.setFromCollideMask(BitMask32.bit(0))
        self.modelGroundCN.setIntoCollideMask(BitMask32.allOff())
        self.modelGroundCNP = self.attachNewNode(self.modelGroundCN)
        if(SHOWCOLL): self.modelGroundCNP.show()

        self.groundHandler = CollisionHandlerQueue()
       
    # This is used for movement
    def setAttribute(self, var, value):
        if(var == MVX):
            self.accel.setX(value)
        elif(var == MVY):
            self.accel.setY(value)
        elif(var == MVCAM):
            self.camrot = value
            
    # Update the ball's rotation based on delta pos
    def updateRotation(self):
        diff = self.getPosDelta()
        axis = Vec3(0,0,1).cross(self.vel)
        axis.normalize()
        angle = (diff.length()/(self.radius/3.0))*(180.0/math.pi)
        self.model.setQuat(self.model.getQuat().multiply(LOrientationf(axis,angle)))
        self.model.node().resetPrevTransform()
        
    def updateXYPosition(self):
        # TODO: Physics! 
        dt = globalClock.getDt()
        
        self.startPos = self.getPos()
        
        self.vel += self.accel * RUNNINGSPEED    
        
        self.vel *= (1-FRICTION_COEFF)

        self.setH(self,self.camrot*TURNINGSPEED)
        
        newX = dt * self.vel[0]
        newY = dt * self.vel[1]
        
        self.setFluidPos(self, newX, newY,0)
        
    def updateZPosition(self):
        dt = globalClock.getDt()
        
        # Make sure that the ball is on top of the ground
        if(self.getZ() <= self.groundZ):
            self.setZ(self.groundZ)
            self.vel.setZ(0)
            return
        
        self.vel.setZ( self.vel[2] + dt * GRAVITY)
        newZ = dt * self.vel[2]
        self.setZ(self, newZ + 0.5)
        
        if(self.getZ() <= self.groundZ):
            self.setZ(self.groundZ)
            self.vel.setZ(-self.vel[2])

        base.camera.lookAt(self.camFollowNode)
#         base.camera.setP(base.camera, (2+self.getZ())/5)
#         base.camera.setZ(base.camera, -(2+self.getZ())/5)
#         base.camera.setP(max(-20, min(-5, base.camera.getP())))
#         base.camera.setPos(0, -200, max(30, min(100, base.camera.getZ())))
        
    def move(self):
        self.updateXYPosition()
        self.updateRotation()
        self.updateZPosition()

##### SHEEP CLASS DEFINITION #####
class Sheep(NodePath):
    def __init__(self, name):
        NodePath.__init__(self, name)
        
        # Initialize attributes
        self.accel = Vec3(0,0,0)
        self.vel = Vec3(0,0,0)
        
        self.animState = ANIM_WALK
        self.animCurr = POSE_ASLEEP
        self.animIsPlaying = False
        self.animCurrPose = 0
        
        self.groundZ = 0
        
        self.ballstate = 0
        self.alivestate = 1
        
#        self.startPos = self.getPos()
        
        # Load the model
        self.model = Actor("models/sheep_mod.egg", 
                           {"start_walk":"models/sheep_ani_walk_start.egg",
                            "walk_loop":"models/sheep_ani_walk.egg",
                            "stop_walk":"models/sheep_ani_walk_stop.egg",
                            "jump_up":"models/sheep_ani_jump_up.egg",
                            "jump_down":"models/sheep_ani_jump_down.egg",
                            "float_up":"models/sheep_ani_float_up.egg",
                            "float_down":"models/sheep_ani_float_down.egg",
                            "flail":"models/sheep_ani_flail.egg",
                            "asleep":"models/sheep_ani_asleep.egg"})
        self.model.setScale(0.1)
        self.model.setH(180)
        self.model.reparentTo(self)
        self.model.pose('asleep', 0) # initial pose
        
        # Setup Collision Ray for ground detection
        self.modelGroundCR = CollisionRay()
        self.modelGroundCR.setOrigin(0,0,1000)
        self.modelGroundCR.setDirection(0,0,-1)
        self.modelGroundCN = CollisionNode(name + 'groundRay')
        self.modelGroundCN.addSolid(self.modelGroundCR)
        self.modelGroundCN.setFromCollideMask(BitMask32.bit(0))
        self.modelGroundCN.setIntoCollideMask(BitMask32.allOff())
        self.modelGroundCNP = self.attachNewNode(self.modelGroundCN)
        if(SHOWCOLL): self.modelGroundCNP.show()

        self.groundHandler = CollisionHandlerQueue()
        
        # Add anim task
        self.taskAnim = taskMgr.add(self.animTask, name + "animTask")
        
    # This is used for movement
    def setAttribute(self, var, value):
        if(var == MVX):
            self.accel.setX(value)
        elif(var == MVY):
            self.accel.setY(value)
        
    def updateXYPosition(self):
        # TODO: Physics! 
        dt = globalClock.getDt()
        
        self.startPos = self.getPos()
        
        self.vel += self.accel * RUNNINGSPEED
        self.vel *= (1-FRICTION_COEFF)
        
        newpos = self.vel * dt
        
        self.setFluidPos(self, newpos[0], newpos[1], newpos[2])
        
    def updateZPosition(self):
        dt = globalClock.getDt()
        
        # Make sure that the ball is on top of the ground
        if(self.getZ() <= self.groundZ):
            self.setZ(self.groundZ)
            self.vel.setZ(0)
            return
        
        self.vel.setZ(self.vel.getZ() + dt * GRAVITY)
        newZ = dt * self.vel[2]
        self.setZ(self, newZ)
        
        if(self.getZ() <= self.groundZ):
            self.setZ(self.groundZ)
            self.vel.setZ(-self.vel.getZ())

    def toball(self, parent):
        self.reparentTo(parent)
        self.vel = Vec3(0,0,0)
        self.accel = Vec3(0,0,0)
        self.ballstate = 1
        self.alivestate = 0
        
        self.modelGroundCN.setFromCollideMask(BitMask32.allOff())
      
    def toworld(self, parent):
        oldpar = self.getParent()
        self.reparentTo(parent)
        self.setPos(oldpar, 0, 0, 0)
        self.ballstate = 0
        
        self.modelGroundCN.setFromCollideMask(BitMask32.bit(0))
        
        taskMgr.doMethodLater(5,self.setalive,'throwsheep',[1])
    
    def isalive(self):
        return self.alivestate
    
    def setalive(self, state):
        self.alivestate = state
        if state:
            self.updateAnim(ANIM_WALK)
    
    def isonball(self):
        return self.ballstate
    
    def move(self):
        if(self.animCurr == ANIM_WALK or self.animCurr == ANIM_JUMP_UP or self.animCurr == ANIM_JUMP_DOWN):
            self.updateXYPosition()
        self.updateZPosition()
        
    def updateAnim(self, anim):
        self.animState = anim
        
    def animTask(self, task):
        if(self.animIsPlaying):
            self.model.pose(ANIM_STRINGS[self.animCurr], self.animCurrPose)
            self.animCurrPose += 1
            if(self.animCurrPose >= ANIM_FRAMES[self.animCurr]):
                # From walk start to walk
                if(ANIM_END_POSE[self.animCurr] == POSE_WALK):
                    self.animCurr = ANIM_WALK
                    self.animCurrPose = 0
                    return Task.cont
                # If loop, loop animation
                elif(ANIM_END_POSE[self.animCurr] == POSE_LOOP):
                    if(self.animState == self.animCurr):
                        self.animCurrPose = 0
                    elif(self.animCurr == ANIM_WALK): # stop walking first
                        self.animCurr = ANIM_WALK_STOP
                        self.animCurrPose = 0
                    else:
                        self.animIsPlaying = False # no transition from flail
                    return Task.cont
                else:
                    self.animCurr = ANIM_END_POSE[self.animCurr]
                    self.animIsPlaying = False
                    return Task.cont
            else:
                # continue animation until end
                return Task.cont
        else:
            if(self.animState == self.animCurr):
                return Task.cont # don't do anything
            
            # No transition to flail
            if(self.animState == ANIM_FLAIL):
                self.animCurr = ANIM_FLAIL
                self.animCurrPose = 0
                self.animIsPlaying = True
                return Task.cont
                
            # No transition to asleep
            if(self.animState == POSE_ASLEEP):
                self.animCurr = POSE_ASLEEP
                self.model.pose('asleep', 0)
                return Task.cont
                
            # From jump up go to stand pose
            if(self.animCurr == POSE_JUMP_UP):
                self.animCurr = ANIM_JUMP_DOWN
                self.animCurrPose = 0
                self.animIsPlaying = True
                return Task.cont
                
            # From floating go to stand pose
            if(self.animCurr == POSE_FLOAT):
                self.animCurr = ANIM_FLOAT_DOWN
                self.animCurrPose = 0
                self.animIsPlaying = True
                return Task.cont
                
            # From asleep go to stand pose
            if(self.animCurr == POSE_ASLEEP):
                self.animCurr = ANIM_WAKE_UP
                self.animCurrPose = 0
                self.animIsPlaying = True
                return Task.cont
                
            # From stand go to any anim
            self.animCurr = self.animState
            self.animCurrPose = 0
            self.animIsPlaying = True
                
        return Task.cont


##### PANDA WORLD CLASS DEFINITION #####
class PandaWorld(DirectObject,FSM.FSM):
    # Method: Initialize Object
    def __init__(self, name): 
        FSM.FSM.__init__(self, name)
        
        # Define states and transitions: OFF -> WAIT -> WORLD -> Cleanup
        self.defaultTransitions={'OFF':['TITLE',],
                                 'TITLE':['INSTRUCTIONS', 'Cleanup'],
                                 'INSTRUCTIONS':['WORLD', 'Cleanup'],
                                 'WORLD':['FINAL', 'Cleanup'],
                                 'FINAL':['Cleanup',]}

        self.setupLoadScreen()
                                 
        # Title Screen Related Attributes 
        cardMaker=CardMaker("TitleScreen")
        cardMaker.setFrame(-4/3.0,4/3.0,-1,1)
        startTexture=loader.loadTexture("textures/title2.tif")
        self.startingScreen=NodePath(cardMaker.generate())
        self.startingScreen.setTexture(startTexture)
        
        # Load World
        self.loadWorld()
        
        self.insCtr = 0
        
        #Hotkeys
        #Disable the mouse camera controls
        base.disableMouse()
        
        #Allow the player to exit at anytime by pressing escape
        self.accept("escape", self.request, extraArgs=["Cleanup"])
              
        #Start World
        #Move into waiting for start state in the FSM
        self.request("TITLE")
        
    # Setup loading screen at startup
    def setupLoadScreen(self):
        self.loadText = OnscreenText("Loading world please wait...",1,fg=(1,1,1,1),
                                     pos=(0,0),align=TextNode.ACenter,scale=.07,mayChange=1)
        for i in range(75):
            base.graphicsEngine.renderFrame()              
        self.loadText.cleanup()
    # end World.setupLoadScreen
        
    def loadWorld(self):
        # Load Environment -- environment already contains collision nodes
        self.environ = loader.loadModel("models/enviro2")      
        self.bed = Actor("models/bed_mod.egg", {"anim":"models/bed_ani.egg"})
        self.bed.loop("anim")
        self.bed.reparentTo(self.environ)
        
        self.environ.reparentTo(render)
        self.environ.setPos(0,0,0)
        self.environ.setScale(0.02)
        
#         # Load Cube
#         self.block = loader.loadModel("models/blockSpiral.egg")
#         self.block.reparentTo(render)
#         self.block.setPos(28.3265, 38.7851, -1.27419)
#         
#         #Collision solid for the block
#         self.blockCS=CollisionSphere(0,0,0,3.5)
#         self.blockCN=CollisionNode("block")
#         self.blockCN.addSolid(self.blockCS)
#         self.blockCN.setFromCollideMask(BitMask32.allOn())
#         self.blockCN.setIntoCollideMask(BitMask32.allOn())
#         self.blockCNP=NodePath(self.blockCN)
#         self.blockCNP.reparentTo(self.block)
#         if SHOWCOLL:self.blockCNP.show()
        
        # Load skydome
        self.sky = loader.loadModel("models/skydome_mod.egg")
        self.sky.setScale(0.00390625 / 2 * 2)
        self.sky.reparentTo(render)
        
        # Setup collision detection
        self.setupCollisions()  

        # Get starting position from the environment
        #ballStartPos = self.environ.find("**/start_point").getPos()
        
        self.sheeplist = []
        self.sheeponball = []
        
        # Load Main Character (sheep ball?)
        self.sheepBall = SheepBall('theSheepBall')
        self.sheepBall.reparentTo(render)
        self.sheepBall.setScale(0.1)
        self.sheepBall.setPos(-9.9, -13.0, -2.1)
        self.sheepBall.setH(-75)
        self.cTrav.addCollider(self.sheepBall.modelGroundCNP, self.sheepBall.groundHandler)
        self.cTrav.addCollider(self.sheepBall.modelCNP, self.sheepBall.groundHandler)
            
        self.isGameDone = False
        self.throwToggle = True
        
        # Initialize camera position and rotation and follow main character
        base.cam.node().getLens().setFar(10000)
        base.camera.setP(-20)
        base.camera.setPos(0,-150,75)
        
        #base.camera.setHpr(0,0,0)
        base.camera.reparentTo(self.sheepBall)      
        
#         self.camGroundRay = CollisionRay()
#         self.camGroundRay.setOrigin(0,0,1000)
#         self.camGroundRay.setDirection(0,0,-1)
#         self.camGroundCol = CollisionNode('camRay')
#         self.camGroundCol.addSolid(self.camGroundRay)
#         self.camGroundCol.setFromCollideMask(BitMask32.allOn())
#         self.camGroundCol.setIntoCollideMask(BitMask32.allOff())
#         self.camGroundColNp = base.camera.attachNewNode(self.camGroundCol)
#         self.camGroundHandler = CollisionHandlerQueue()
#         self.cTrav.addCollider(self.camGroundColNp, self.camGroundHandler)

        # Lighting
        # Ambient light
        alight = AmbientLight('alight')
        alight.setColor(VBase4(0.5,0.5,0.5,1.0))
        alnp = render.attachNewNode(alight)
        render.setLight(alnp)
        
        # Directional Light
        keyLight = DirectionalLight('keyLight')
        keyLight.setColor(VBase4(0.3,0.3,0.3,1.0))
        keyLightNP = render.attachNewNode(keyLight.upcastToPandaNode())
        keyLightNP.setHpr(VBase3(-45.0,-45.0,0.0))
        render.setLight(keyLightNP)
        
        # Fog
        self.fog = Fog('distanceFog')
        self.fog.setColor(0.5, 0.5, 0.5)
        self.fog.setExpDensity(0.0004)
        render.setFog(self.fog)     
        
        # Normal Mapping
        render.setShaderAuto()
        
        # Toon Shading
        # Check if video card supports shading
        if (base.win.getGsg().getSupportsBasicShaders() != 0):
            # Enable a 'light ramp' - this discretizes the lighting,
            # which is half of what makes a model look like a cartoon.
            # Light ramps only work if shader generation is enabled,
            # so we call 'setShaderAuto'.
    
            tempnode = NodePath(PandaNode("temp node"))
            tempnode.setAttrib(LightRampAttrib.makeSingleThreshold(0.5, 0.4))
            tempnode.setShaderAuto()
            base.cam.node().setInitialState(tempnode.getState())
            
            # Use class 'CommonFilters' to enable a cartoon inking filter.
            # This can fail if the video card is not powerful enough, if so,
            # display an error and exit.
    
            self.separation = 1.5 # Pixels
            self.filters = CommonFilters(base.win, base.cam)
            self.filters.setCartoonInk(separation=self.separation)

        # Load Sfx
        self.loadSfx()        
        
    def setupCollisions(self):
        self.cTrav = CollisionTraverser()
        
        # Set as default collision traverser
        base.cTrav = self.cTrav
        if(SHOWCOLL): self.cTrav.showCollisions(render)
    
    def dot_product(self, v1, v2):
      return v1[0] * v2[0] + v1[1] * v2[1] + v1[2] * v2[2]
    
    def handleCollisions(self):
        # Traverse the collision tree and populate queues
        self.cTrav.traverse(render)
        
        # Handle sheep ball collisions
        entries = []
        sentries = []
        centries = []
        sheepnum = 0

        for i in range(self.sheepBall.groundHandler.getNumEntries()):
            entry = self.sheepBall.groundHandler.getEntry(i)
            if entry.getFromNode().getName() == "theSheepBallgroundRay":
                entries.append(entry)
            else:
                sentries.append(entry)
                
        entries.sort(lambda x,y: cmp(y.getSurfacePoint(render).getZ(),
                                     x.getSurfacePoint(render).getZ()))
        sentries.sort(lambda x,y: cmp(y.getSurfacePoint(render).getZ(),
                                     x.getSurfacePoint(render).getZ()))
                             
        grav = Vec3(0, 0, -9.81)
        totalnormal = Vec3(0, 0, 0)
        final_force = grav
        if (len(entries)>0) and (entries[0].getIntoNode().getName() == "ground"):
            # TODO: Implement physics here! -- sheepball collision ray is not 
            # hitting anything else but the ground
            # This doesn't mean that the sheepball is on the ground!
            # self.sheepBall.setZ(entries[0].getSurfacePoint(render).getZ() + 0.3)
            normal = entries[0].getSurfaceNormal(render) 
            
            reaction_force = normal * self.dot_product(grav, -normal) / normal.length() / normal.length()
            
            final_force += reaction_force/2.0
            
            
            self.sheepBall.groundZ = entries[0].getSurfacePoint(render).getZ() + self.sheepBall.radius/10 + 0.25
            sheepnum += math.floor(reaction_force.length() / 40)
                    
        else:
            # The sheep ball is out of bounds so don't make it move
            self.sheepBall.setPos(self.sheepBall.startPos)
            
        if(len(sentries)>0) and (sentries[0].getIntoNode().getName() != "ground"):
            # TODO: Implement physics here! -- sheepball hit something else
            # self.sheepBall.setPos(self.sheepBall.startPos)
            normal = sentries[0].getSurfaceNormal(render)
            totalnormal += normal
            
            reaction_force = normal * self.dot_product(self.sheepBall.vel, -normal) / normal.length() / normal.length()
            self.sheepBall.vel += reaction_force
            final_force += reaction_force
            sheepnum += math.floor(reaction_force.length() / 40)
            
            #sheepnum += math.floor(math.sqrt(self.sheepBall.vel[0]**2 + self.sheepBall.vel[1]**2 + self.sheepBall.vel[2]**2)/50)
            # TODO use getInteriorPoint to prevent getting stuck
            
        if sheepnum:
            sheepnum = min(len(self.sheeponball),min(4,sheepnum))
        
            # throw the sheep
            if(self.throwToggle):
                for i in range(0,sheepnum):
                    sheep = self.popSheep()
                    sheep.vel.setX(random.randint(-1, 1))
                    sheep.vel.setY(random.randint(-1, 1))
                    sheep.vel.setZ(random.randint(1,2))
                self.toggleThrow()
                taskMgr.doMethodLater(0.5, self.toggleThrow, 'throwlater', [])

        # handle camera collisions
#         for i in range(self.camGroundHandler.getNumEntries()):
#             entry = self.camGroundHandler.getEntry(i)
#             centries.append(entry)
#         centries.sort(lambda x,y: cmp(y.getSurfacePoint(render).getZ(),
#                                      x.getSurfacePoint(render).getZ()))
#         if (len(centries)>0):
#             base.camera.setZ(render, centries[0].getSurfacePoint(render).getZ()+1.0)
#         if (base.camera.getZ() < self.sheepBall.getZ() + 25.0):
#             base.camera.setZ(25.0)
                
        # handle sheep collisions
        for sheep in self.sheeplist:
            if not sheep.isonball():
                sheep.vel += grav
                entries = []
                for i in range(sheep.groundHandler.getNumEntries()):
                    entry = sheep.groundHandler.getEntry(i)
                    entries.append(entry)
                entries.sort(lambda x,y: cmp(y.getSurfacePoint(render).getZ(),
                                             x.getSurfacePoint(render).getZ()))
                if (len(entries)>0) and (entries[0].getIntoNode().getName() == "ground"):
                    # TODO: Implement physics here! -- sheepball collision ray is not 
                    # hitting anything else but the ground
                    # This doesn't mean that the sheepball is on the ground!
                    # self.sheepBall.setZ(entries[0].getSurfacePoint(render).getZ() + 0.3)
                    sheep.groundZ = entries[0].getSurfacePoint(render).getZ() + 0.3
                elif(len(entries)>0):
                    # TODO: Implement physics here! -- sheepball hit something else
                    # self.sheepBall.setPos(self.sheepBall.startPos)
                    normal = entries[0].getSurfaceNormal(render)
#                     if(normal[0] != 0):
#                         sheep.vel.setX(-sheep.vel[0])
#                     if(normal[1] != 0):
#                         sheep.vel.setY(-sheep.vel[1])
                else:
                    # The sheep ball is out of bounds so don't make it move
                    # remove anim task first
                    taskMgr.remove(sheep.taskAnim)
                    sheep.removeNode()
                    self.sheeplist.remove(sheep)
        self.sheepBall.vel += final_force

    
    def addSheep(self):
        # add a new sheep to the world
        sheep = Sheep(str(len(self.sheeplist)))
        self.sheeplist.append(sheep)
        sheep.reparentTo(render)
        sheep.setScale(0.1)
        sheep.setPos(self.sheepBall, random.randint(-30,30),
          random.randint(-30,30), random.randint(-30,30))
        self.cTrav.addCollider(sheep.modelGroundCNP, sheep.groundHandler)
        
    def toggleThrow(self):
        self.throwToggle = not self.throwToggle
        
    def pushSheep(self,sheep):
        # put a sheep on the ball if it isnt on it already
        if not sheep.isonball():
            self.sheeponball.append(sheep)
            # set the radius depending on the number of sheep on the ball
            self.sheepBall.radius = max(2*math.sqrt(len(self.sheeponball)),3)
            self.sheepBall.modelCS.setRadius(self.sheepBall.radius)
            # set the position on the sphere randomly
            sheephangle = random.uniform(-math.pi,math.pi)
            sheepangle = random.uniform(0,2*math.pi)
            # convert polar coords back to cartesian
            x = (self.sheepBall.radius)*math.sin(sheephangle)*math.cos(sheepangle)
            y = (self.sheepBall.radius)*math.cos(sheephangle)
            z = (self.sheepBall.radius)*math.sin(sheephangle)*math.sin(sheepangle)
            # move the sheep into the right position, face it randomly
            sheep.toball(self.sheepBall.model)            
            sheep.setPosHpr(x,y,z+3,random.randint(0,360),
              random.randint(0,360),random.randint(0,360))
            sheep.setScale(1)
            
            self.sheepAmbVol += 0.1
            self.sheepAmbVol = min(self.sheepAmbVol, 1)
            self.sheepAmb.setVolume(self.sheepAmbVol)
            sheep.updateAnim(ANIM_FLAIL)
            
            
      
    def popSheep(self):
        # if there are any sheep left on the ball
        if len(self.sheeponball):
            # grab the last sheep that was added to it
            sheep = self.sheeponball.pop()
            self.sheepBall.radius = max(2*math.sqrt(len(self.sheeponball)),3)
            self.sheepBall.modelCS.setRadius(self.sheepBall.radius)
            # make its parent the world
            sheep.toworld(render)
            sheep.setScale(0.1)
            sheep.updateAnim(POSE_ASLEEP)
            
            # play sheep sfx
            self.sheepSfx[random.randint(0, 5)].play()
            
            self.sheepAmbVol -= 0.1
            self.sheepAmbVol = max(self.sheepAmbVol, 0)
            self.sheepAmb.setVolume(self.sheepAmbVol)
            
            return sheep
        
    def sheepTask(self, task):
        if(len(self.sheeplist) < MAX_SHEEP):
            self.addSheep()
        # move all free roaming sheep
        for sheep in self.sheeplist:
            if sheep.isalive():
                if sheep.getDistance(self.sheepBall) < (self.sheepBall.radius*1.2) + 3:
                    self.pushSheep(sheep)
                elif sheep.getDistance(self.sheepBall) < (self.sheepBall.radius*1.2) + 20:
                    sheep.updateAnim(ANIM_JUMP_UP)
                else:
                    sheep.updateAnim(ANIM_WALK)
                    if(self.sheepBall.getX() > sheep.getX()):
                        sheep.setAttribute(MVX, -0.5)
                    else:
                        sheep.setAttribute(MVX, 0.5)
                        
                    if(self.sheepBall.getY() > sheep.getY()):
                        sheep.setAttribute(MVY, -0.5)
                    else:
                        sheep.setAttribute(MVY, 0.5)
                    sheep.lookAt(self.sheepBall)
        return Task.cont
        
    def loadSfx(self):
        self.sheepAmbVol = 0.0
        self.sheepAmb = loader.loadSfx('sounds/sheepamb.wav')
        self.sheepSfx = [loader.loadSfx('sounds/lamb.wav'),
                         loader.loadSfx('sounds/sheep.wav'),
                         loader.loadSfx('sounds/sheep2.wav'),
                         loader.loadSfx('sounds/sheep3.wav'),
                         loader.loadSfx('sounds/sheep4.wav'),
                         loader.loadSfx('sounds/sheep5.wav')]
                         
        self.alarmSfx = loader.loadSfx('sounds/alarm_clock.wav')
        self.hitSfx = [loader.loadSfx('sounds/splat.wav'),
                       loader.loadSfx('sounds/thud.wav')]
        
    def updateWorldTask(self, task):
        vector = self.sheepBall.getPos() - Point3(5.50996, -11.1235, -0.677819)
        if((vector.length() <= 3) and (len(self.sheeponball) == 0) and (not self.isGameDone)):
            self.request('FINAL')
        for sheep in self.sheeplist:
            if not sheep.isonball():
                sheep.move()
        self.sheepBall.move()
        self.handleCollisions()
        return Task.cont
        
    def acceptSpace(self):
        if(self.insCtr == 0):
            # second page instructions
            titleTex = loader.loadTexture('textures/instructions.png')
            self.startingScreen.setTexture(titleTex)
        elif(self.insCtr == 1):
            # third page instructions
            titleTex = loader.loadTexture('textures/instructions.png')
            self.startingScreen.setTexture(titleTex)
        else:
            self.request("WORLD")
        self.insCtr += 1
        
    #FSM States            
    def enterTITLE(self):
        # Listen for space bar, and start the main world when its pressed
        self.accept("space",self.request,extraArgs=["INSTRUCTIONS"])
        # Display the startup screen
        self.startingScreen.reparentTo(aspect2d)
        
        bgMusic = loader.loadSfx("sounds/rhapsody.mp3")
        bgMusic.setLoop(True)
        bgMusic.setLoopCount(0)
        bgMusic.play()

    def exitTITLE(self):
        # Clean up the visuals and event handling from the state
        self.startingScreen.hide()
        self.ignore("space")
        
    def enterINSTRUCTIONS(self):    
        # Show instructions
        titleTex = loader.loadTexture('textures/instructions.png')
        self.startingScreen.setTexture(titleTex)
        self.startingScreen.setTransparency(1)
        self.startingScreen.show()

        # Populate Sheep
        for i in range(MAX_SHEEP):
            self.addSheep()
            
        # Play ambient sound
        self.sheepAmb.setVolume(self.sheepAmbVol)
        self.sheepAmb.setLoop(True)
        self.sheepAmb.setLoopCount(0)
        self.sheepAmb.play()
        
        # Add task that will move camera position
        taskMgr.add(self.sheepTask, "sheepTask")
        taskMgr.add(self.updateWorldTask, "updateWorld")
        
        # Wait for space bar
        self.accept("space", self.acceptSpace)
    
    def exitINSTRUCTIONS(self):
        # Clean up the instructions
        self.startingScreen.hide()
        
        self.ignore("space")
        
    def enterWORLD(self):        
        # Movement control
        self.accept("w",self.sheepBall.setAttribute, [MVY, 1])
        self.accept("s",self.sheepBall.setAttribute, [MVY, -1])
        self.accept("q",self.sheepBall.setAttribute, [MVX, -1])
        self.accept("e",self.sheepBall.setAttribute, [MVX, 1])
        self.accept("a",self.sheepBall.setAttribute, [MVCAM,1])
        self.accept("d",self.sheepBall.setAttribute, [MVCAM,-1])        
        self.accept("w-up",self.sheepBall.setAttribute, [MVY, 0])
        self.accept("s-up",self.sheepBall.setAttribute, [MVY, 0])
        self.accept("q-up",self.sheepBall.setAttribute, [MVX, 0])
        self.accept("e-up",self.sheepBall.setAttribute, [MVX, 0])
        self.accept("a-up",self.sheepBall.setAttribute, [MVCAM,0])
        self.accept("d-up",self.sheepBall.setAttribute, [MVCAM,0])
        self.accept("o", base.oobe)
        
        globalClock.reset()
        self.startTime = globalClock.getRealTime()
        
        pass

    def exitWORLD(self):
        self.endTime = globalClock.getRealTime()
        pass
    
    def enterFINAL(self):
        taskMgr.remove('updateWorld')
        taskMgr.remove('sheepTask')
        self.isGameDone = True
        self.alarmSfx.setLoop(False)
        
        clockPos = base.camera.getPos(self.sheepBall) + Point3(0, 50, -17)
        
        clockInt1 = LerpHprInterval(self.sheepBall.model, 2, VBase3(0, 0, 0))
        clockInt2 = LerpPosInterval(self.sheepBall.model, 2, Point3(clockPos))
        clockInt = Parallel(clockInt1, clockInt2)
        
        clockInt.start()
        self.sheepBall.dummy.loop('anim')
        self.alarmSfx.play()
        
        self.ignore("q")
        self.ignore("w")
        self.ignore("e")
        self.ignore("a")
        self.ignore("s")
        self.ignore("d")
        self.ignore("o")
        
        # Show score
        if(self.endTime <= 30.0):
            titleTex = loader.loadTexture('textures/rank1.tif')
        elif(self.endTime <= 60.0):
            titleTex = loader.loadTexture('textures/rank2.tif')
        elif(self.endTime <= 90.0):
            titleTex = loader.loadTexture('textures/rank3.tif')
        elif(self.endTime <= 120.0):
            titleTex = loader.loadTexture('textures/rank4.tif')
        else:
            titleTex = loader.loadTexture('textures/rank5.tif')
        self.startingScreen.setTexture(titleTex)
        self.startingScreen.setTransparency(1)
        self.startingScreen.show()
        
        # Wait for the player to press escape
        self.accept("escape", self.request, extraArgs=["Cleanup"]) 
        pass
    
    def exitFinal(self):
        pass
        
    def enterCleanup(self):
        #ignore all keyboard inputs
        self.ignoreAll()
        #close panda
        sys.exit()
        
    def exitCleanup(self):
        pass 
    
pw=PandaWorld("myWorld")
run()
